Chrome DevTools extension for visualizing network requests, detecting N+1 patterns, replaying requests, and decoding JWTs. Built with React 19, TanStack DB, and TypeScript.
A Chrome DevTools extension for visualizing network requests, detecting N+1 query patterns, replaying requests, and decoding JWTs. Built for developers who want to debug API calls, identify performance bottlenecks, and inspect authentication tokens.
Captures XHR, Fetch, and Document requests made by the current page. Unlike the built-in Network tab, this extension focuses specifically on API calls and provides specialized views for debugging.
Automatically groups requests by URL pattern to identify N+1 query problems:
/api/users/1, /api/users/2, /api/users/3 → grouped as /api/users/:id (3x)Track requests across page navigations within a browsing session:
github.com, api.github.com)/, /repos, /settings)Replay any captured request with full editing capabilities:
Automatically detects and decodes JWT tokens in request headers:
Authorization, X-Auth-Token, or custom headersExport request data for documentation or debugging:
["key: value"] arrayCustomize the extension behavior:
# Install dependencies
pnpm install
# Development build with watch mode
pnpm dev
# Production build
pnpm build
pnpm build to create the dist folderchrome://extensionsdist folder from this projectF12 or Cmd+Option+I on Mac / Ctrl+Shift+I on Windows/Linux)After making code changes:
pnpm build (or keep pnpm dev running for auto-rebuild)chrome://extensionsTip: You must close and reopen DevTools for panel changes to take effect. Simply refreshing the page won't update the DevTools panel.
chrome://extensions → Click "Service Worker" link under the extension| Feature | Description |
|---|---|
| Grouped View | Groups similar URLs together with count badges (e.g., 3x /api/users/:id) |
| Flat View | Shows all requests chronologically |
| Search | Filter requests by URL or method |
| Method Filter | Show only GET, POST, PUT, PATCH, DELETE, or OPTIONS |
| Sort Options | Sort by newest, oldest, method, or status |
| Request Details | Click any request to see headers, body, response, and JWT info |
| Copy JSON | Export full request details as formatted JSON |
| Replay | Re-send any request with editable parameters |
| Feature | Description |
|---|---|
| Domain Groups | Requests organized by domain with total counts |
| Page Groups | Within each domain, grouped by page path |
| Expandable | Click to expand/collapse domains and pages |
| Copy Summary | Export domain or page requests as JSON |
| Clear Controls | Clear individual pages or entire domains |
| Setting | Description |
|---|---|
| Session Retention | How long to keep request history (1 hour to 1 week) |
| JWT Headers | Which headers to scan for JWT tokens |
| Theme | Light, dark, or system preference |
package.json: "version": "1.0.0"
pnpm build
dist folder as unpacked extension cd dist
zip -r ../request-visualizer.zip .
public/icon/128.png)request-visualizer.zippackage.jsonpnpm builddist folderThis extension uses TanStack DB for reactive client-side data management. Here are the key patterns we use:
import { createCollection, createLocalStoragePersister } from "@tanstack/db";
// Define schema
const requestsSchema = {
id: "string",
url: "string",
method: "string",
status: "number",
startTime: "number",
// ...
} as const;
// Create collection with persistence
export const requestsCollection = createCollection({
id: "requests",
schema: requestsSchema,
persister: createLocalStoragePersister({ name: "requests-db" }),
});
import { useLiveQuery } from "@tanstack/react-db";
import { eq, ilike, or } from "@tanstack/db";
// Reactive filtered query - re-executes when dependencies change
const filteredResult = useLiveQuery(
(q) => {
let query = q.from({ req: requestsCollection });
// Method filter - eq() for equality
if (filters.method !== "ALL") {
query = query.where(({ req }) => eq(req.method, filters.method));
}
// Search filter - ilike() for case-insensitive pattern matching
if (filters.search) {
query = query.where(({ req }) =>
or(
ilike(req.url, `%${filters.search}%`),
ilike(req.method, `%${filters.search}%`)
)
);
}
// Sorting
return query.orderBy(({ req }) => req.startTime, "desc");
},
[filters.method, filters.search] // Dependencies trigger re-execution
);
// Insert
requestsCollection.utils.writeInsert(newRequest);
// Delete
requestsCollection.utils.writeDelete(requestId);
// Update
requestsCollection.utils.writeUpdate(requestId, { status: 200 });
import { eq, gt, gte, lt, lte, like, ilike, inArray, and, or, not } from "@tanstack/db";
// Equality
eq(req.id, 1)
// Comparisons
gt(req.status, 200) // greater than
gte(req.status, 200) // greater than or equal
lt(req.status, 400) // less than
lte(req.status, 400) // less than or equal
// String matching
like(req.url, "/api/%") // case-sensitive pattern
ilike(req.url, "/api/%") // case-insensitive pattern
// Array membership
inArray(req.method, ["GET", "POST"])
// Logical operators
and(condition1, condition2)
or(condition1, condition2)
not(condition)
// Chain .where() calls conditionally - each adds an AND condition
const result = useLiveQuery(
(q) => {
let query = q.from({ req: requestsCollection });
if (showActive) {
query = query.where(({ req }) => eq(req.active, true));
}
if (minStatus) {
query = query.where(({ req }) => gte(req.status, minStatus));
}
return query.orderBy(({ req }) => req.startTime, "desc");
},
[showActive, minStatus]
);
MIT